package com.bgyrobot.coordinate;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.math3.linear.Array2DRowRealMatrix;
import org.apache.commons.math3.linear.RealMatrix;
import org.locationtech.proj4j.io.Proj4FileReader;

import com.bgyrobot.coordinate.enumeration.CoordinateSystemTypeEnum;
import com.bgyrobot.coordinate.model.CoordinateSystem;
import com.bgyrobot.coordinate.model.CoordinateTransform;
import com.bgyrobot.coordinate.model.ReferencePoint;
import com.bgyrobot.coordinate.util.MatrixUtil;

/**
 * 综合地理转换，可缓存耗时计算
 * @author hesu
 */
public class Transform implements ITransform {
    
    public static final String[] GEO_AUTHS = new String[] {"epsg", "esri", "nad27", "nad83", "world"};
    public static final String GEOCODE_PATTERN = "(?<=^<)[^>]+(?=>)";

    private ICoordinateSystemProvider localProvider;
    private Geotransform geoTransform;
    private Map<String, CoordinateSystem> csPool;
    private Map<String, CoordinateTransform> transformPool;
    
    public Transform(ICoordinateSystemProvider localProvider) {
        this.localProvider = localProvider;
        this.geoTransform = new Geotransform();
        this.transformPool = new HashMap<String, CoordinateTransform>();
        this.csPool = new HashMap<String, CoordinateSystem>();
    }
    
    public ICoordinateSystemProvider getLocalProvider() {
        return this.localProvider;
    }
    
    public CoordinateSystem getCoordinateSystem(String csName) {
        if (csName == null) {
            return null;
        }
        
        CoordinateSystem cs = csPool.get(csName);
        if (cs != null) {
            return cs;
        }
            
        if (Pattern.matches(CoordinateSystem.LOCALCS_NAME_PATTERN, csName)) {
            cs = localProvider.query(csName);
            if (cs == null) {
                return null;
            }
            cacheAtomicTransform(cs);
            csPool.put(csName, cs);
            return cs;
        }
        
        if (Pattern.matches(CoordinateSystem.GEOCS_NAME_PATTERN, csName)) {
            if (geoTransform.getCoordinateSystem(csName) == null) {
                return null;
            }
            cs = new CoordinateSystem(csName);
            cs.setType(CoordinateSystemTypeEnum.GEOGRAPHIC);
            csPool.put(csName, cs);
            
            return cs;
        }

        return null;
    }
    
    public CoordinateTransform getCoordinateTransform(String srcCS, String tgtCS, ReferencePoint refPoint) {
        if (srcCS == null || tgtCS == null) {
            return null;
        }
        
        if (srcCS.equalsIgnoreCase(tgtCS)) {
            return CoordinateTransform.SAME;
        }
        
        if (refPoint != null && !refPoint.valid()) {
            return null;
        }
        
        String key = CoordinateTransform.hashString(srcCS, tgtCS, refPoint);
        CoordinateTransform trans = transformPool.get(key);
        if (trans != null) {
            return trans;
        }
        
        if (refPoint != null) {
            // 尝试不需要 refPoint
            String key2 = CoordinateTransform.hashString(srcCS, tgtCS, null);
            trans = transformPool.get(key2);
            if (trans != null) {
                return trans;
            }
        }
        
        List<CoordinateSystem> stack = getTransformStack(srcCS, tgtCS);
        CoordinateTransform geoTransform = findInnerGeotransform(stack);
        
        
        if (geoTransform != null) {
            // 存在地理变换
            if (refPoint == null) {
                return null;
            }
            ReferencePoint newRefPoint = toGeoReferencePoint(refPoint);
            double[][] mat = getGeotransformApproximateMatrix(geoTransform, newRefPoint);
            geoTransform.setRefPoint(refPoint);
            geoTransform.setMatrix(mat);
            transformPool.put(geoTransform.toString(), geoTransform);
            
            CoordinateTransform geoReverse = geoTransform.reverse();
            transformPool.put(geoReverse.toString(), geoReverse);
        } else {
            refPoint = null;
        }

        RealMatrix matrix = MatrixUtil.unitMatrix(false);
        for (int i = 0; i < stack.size() - 1; i++) {
            String cs1 = stack.get(i).getName();
            String cs2 = stack.get(i + 1).getName();
            ReferencePoint curRefPoint = null;
            if (geoTransform != null && cs1.equalsIgnoreCase(geoTransform.getSrcCS()) && cs2.equalsIgnoreCase(geoTransform.getTgtCS())) {
                curRefPoint = refPoint;
            }
            String curKey = CoordinateTransform.hashString(cs1, cs2, curRefPoint);
            CoordinateTransform curTransform = transformPool.get(curKey);
            if (curTransform == null) {
                return null;
            }
            RealMatrix curMatrix = new Array2DRowRealMatrix(curTransform.getMatrix());
            matrix = matrix.multiply(curMatrix);
        }

        trans = new CoordinateTransform(srcCS, tgtCS, refPoint, matrix.getData());
        
        transformPool.put(trans.toString(), trans);
        
        return trans;
    }

    public boolean cacheCoordinateSystem(CoordinateSystem cs, boolean isSave) {
        if (cs == null) {
            return false;
        }
        String key = cs.toString();
        if (isSave) {
            if (!cacheAtomicTransform(cs)) {
                return false;
            }
            csPool.put(key, cs);
            return true;
        }
        if (csPool.containsKey(key)) {
            if (!uncacheRelatedTransform(key)) {
                return false;
            }
            csPool.remove(key);
            return true;
        }
        return true;
    }
    
    public void cacheCoordinateTransform(CoordinateTransform transform, boolean isSave) {
        if (transform == null) {
            return;
        }
        String key = transform.toString();
        if (isSave) {
            transformPool.put(key, transform);
            return;
        }
        if (transformPool.containsKey(key)) {
            transformPool.remove(key);
        }
    }

    public CoordinateTransform[] getCoordinateTransformTable(String[] csNames, ReferencePoint refPoint) {
        int count = csNames.length;
        CoordinateTransform[] table = new CoordinateTransform[(count - 1) * count];
        int idx = 0;
        for (int i = 0; i < count; i++) {
            for (int j = 0; j < count; j++) {
                if (i != j) {
                    table[idx++] = getCoordinateTransform(csNames[i], csNames[j], refPoint);
                }
            }
        }
        return table;
    }
    
    public boolean hasGeotransform(String srcCS, String tgtCS) {
        return hasGeotransform(getTransformStack(srcCS, tgtCS));
    }
    
    public double[][] getMatrixWithGeotransform(String srcCS, String tgtCS, ReferencePoint refPoint) {
        CoordinateTransform transform = getCoordinateTransform(srcCS, tgtCS, refPoint);
        if (transform == null) {
            return null;
        }
        return transform.getMatrix();
    }
    
    public double[][] getMatrixWithoutGeotransform(String srcCS, String tgtCS) {
        CoordinateTransform transform = getCoordinateTransform(srcCS, tgtCS, null);
        if (transform == null) {
            return null;
        }
        return transform.getMatrix();
    }
    
    public double[][] executeWithoutGeotransform(String srcCS, String tgtCS, double[][] coords) {
        CoordinateTransform trans = getCoordinateTransform(srcCS, tgtCS, null);
        if (trans == null) {
            return null;
        }
        return MatrixUtil.transform(trans.getMatrix(), coords, false);
    }
    
    public double[][] executeWithGeotransform(String srcCS, String tgtCS, ReferencePoint refPoint, double[][] coords) {
        CoordinateTransform trans = getCoordinateTransform(srcCS, tgtCS, refPoint);
        if (trans == null) {
            return null;
        }
        return MatrixUtil.transform(trans.getMatrix(), coords, false);
    }
    
    public double[][] executeWithNativeGeotransform(String srcCS, String tgtCS, double[][] coords) {
        
        List<CoordinateSystem> stack = getTransformStack(srcCS, tgtCS);
        
        CoordinateTransform geoTrans = findInnerGeotransform(stack);
        if (geoTrans == null) {
            return executeWithoutGeotransform(srcCS, tgtCS, coords);
        }
        
        double[][] result = coords;
        if (! srcCS.equalsIgnoreCase(geoTrans.getSrcCS())) {
            result = executeWithoutGeotransform(srcCS, geoTrans.getSrcCS(), result);
            if (result == null) {
                return null;
            }
        }
        result = geoTransform.execute(geoTrans.getSrcCS(), geoTrans.getTgtCS(), result, false);
        if (result == null) {
            return null;
        }
        if (! tgtCS.equalsIgnoreCase(geoTrans.getTgtCS())) {
            result = executeWithoutGeotransform(geoTrans.getTgtCS(), tgtCS, result);
            if (result == null) {
                return null;
            }
        }
        return result;
    }
    
    public double[][] executeMatrixTransform(double[][] matrix, double[][] coords) {
        return MatrixUtil.transform(matrix, coords, false);
    }
    
    public String[] getGeoCoordinateSystems() {
        List<String> names = new ArrayList<String>();
        
        for (String auth : GEO_AUTHS) {
            try {
                InputStream inputStream = Proj4FileReader.class.getClassLoader().getResourceAsStream("proj4/nad/" + auth);
                
                byte b[] = new byte[1024*1024] ;        // 数组大小由文件决定
                int len = 0 ; 
                int temp = 0 ;            // 接收每一个读取进来的数据
                while((temp=inputStream.read())!=-1){
                     // 表示还有内容，文件没有读完
                     b[len] = (byte)temp ;
                     len++ ;
                }
                String tmpstr = new String(b,0,len);
                Pattern p = Pattern.compile(GEOCODE_PATTERN, Pattern.MULTILINE);
                Matcher m = p.matcher(tmpstr); // 获取 matcher 对象
                while(m.find()) {
                    String code = tmpstr.substring(m.start(), m.end());
                    names.add(auth + ":" + code);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        return names.toArray(new String[0]);
    }
    
    
    // private methods ====================================================
    
    private boolean cacheAtomicTransform(CoordinateSystem cs) {
        
        double[][] mat = cs.resolve();
        if (mat == null) {
            return false;
        }

        String name = cs.getName();
        String baseName = cs.getBaseCS();
        CoordinateTransform trans = new CoordinateTransform(baseName, name, null, mat);
        transformPool.put(trans.toString(), trans);
        
        CoordinateTransform reverse = trans.reverse();
        transformPool.put(reverse.toString(), reverse);
        
        return true;
    }
    
    private boolean uncacheRelatedTransform(String name) {
        Iterator<Entry<String, CoordinateTransform>> entries = transformPool.entrySet().iterator();
        while(entries.hasNext()){
            Entry<String, CoordinateTransform> entry = entries.next();
            String key = entry.getKey();
            if (key.indexOf(name) > -1) {
                entries.remove();
            }
        }
        
        return true;
    }
    
    private List<CoordinateSystem> getCoordinateStack(String name) {
        ArrayList<CoordinateSystem> stack = new ArrayList<CoordinateSystem>();
        CoordinateSystem cs = getCoordinateSystem(name);
        while (cs != null) {
            stack.add(cs);
            if (cs.getType() == CoordinateSystemTypeEnum.GEOGRAPHIC) {
                return stack;
            }
            name = cs.getBaseCS();
            cs = getCoordinateSystem(name);
        }
        if (name != null && !name.equalsIgnoreCase("")) {
            stack.add(new CoordinateSystem(name));
        }
        return stack;
    }
    
    private List<CoordinateSystem> getTransformStack(String srcName, String tgtName) {
        List<CoordinateSystem> stack = new ArrayList<CoordinateSystem>();
        if (tgtName.equalsIgnoreCase(srcName)) {
            CoordinateSystem cs = getCoordinateSystem(srcName);
            if (cs != null) {
                cs = new CoordinateSystem(srcName);
            }
            stack.add(cs);
            return stack;
        }
        
        stack = getCoordinateStack(srcName);
        List<CoordinateSystem> stackTail = getCoordinateStack(tgtName);
        int trimSrcIndex = 1000;
        int trimTgtIndex = 1000;
        int sum = 2000;
        for (int i = 0; i < stack.size(); i++) {
            String srcNameTmp = stack.get(i).getName();
            for (int j = 0; j < stackTail.size(); j++) {
                String tgtNameTmp = stackTail.get(j).getName();
                if (srcNameTmp.equalsIgnoreCase(tgtNameTmp) && i + j < sum) {
                    sum = i + j;
                    trimSrcIndex = i;
                    trimTgtIndex = j;
                }
            }
        }
        if (sum < 2000) {
            stack = new ArrayList<CoordinateSystem>(stack.subList(0, trimSrcIndex + 1));
            stackTail = new ArrayList<CoordinateSystem>(stackTail.subList(0, trimTgtIndex));
        }
        Collections.reverse(stackTail);
        stack.addAll(stackTail);
        return stack;
    }
    
    /**
     * 如果栈中有 地理变换，则返回地理变换。没有则返回null
     */
    private CoordinateTransform findInnerGeotransform(List<CoordinateSystem> stack) {
        for (int i = 0; i < stack.size() - 1; i++) {
            CoordinateSystem cs1 = stack.get(i);
            CoordinateSystem cs2 = stack.get(i + 1);
            if (cs1.getType() == CoordinateSystemTypeEnum.GEOGRAPHIC
                    && cs2.getType() == CoordinateSystemTypeEnum.GEOGRAPHIC) {
                return new CoordinateTransform(cs1.getName(), cs2.getName(), null, null);
            }
        }
        return null;
    }
    
    private boolean hasGeotransform(List<CoordinateSystem> stack) {
        CoordinateTransform trans = findInnerGeotransform(stack);
        return trans != null;
    }
    
    /**
     * 将参考点 转为另一 地理坐标系中的参考点
     */
    private ReferencePoint convertGeoReferencePoint(ReferencePoint refPoint, String tgtGeoCS) {

        String srcGeoCS = refPoint.getRefCS();
        if (srcGeoCS.equalsIgnoreCase(tgtGeoCS)) {
            return refPoint;
        }
        
        double[][] refCoords = new double[][] { refPoint.getRefCoord() };
        double[][] outCoords = geoTransform.execute(srcGeoCS, tgtGeoCS, refCoords, false);
        return new ReferencePoint(tgtGeoCS, outCoords[0]);
    }
    
    /**
     * 获取 地理转换的 地理近似解转换
     */
    private double[][] getGeotransformApproximateMatrix(CoordinateTransform transform, ReferencePoint refPoint) {
        
        String srcCS = transform.getSrcCS();
        String tgtCS = transform.getTgtCS();
        String refCS = refPoint.getRefCS();
        
        if (!refCS.equalsIgnoreCase(srcCS) && !refCS.equalsIgnoreCase(tgtCS)) {
            refPoint = convertGeoReferencePoint(refPoint, srcCS.compareToIgnoreCase(tgtCS) < 0 ? srcCS : tgtCS);
        }
        double[][] matrix = geoTransform.geotransformToMatrix(srcCS, tgtCS, refPoint);
        return matrix;
    }
    
    /**
     * 将相对坐标系中的点 转为 地理坐标系中的点
     */
    private ReferencePoint toGeoReferencePoint(ReferencePoint refPoint) {
        String refCS = refPoint.getRefCS();
        CoordinateSystem cs = getCoordinateSystem(refCS);
        if (cs.getType() == CoordinateSystemTypeEnum.GEOGRAPHIC) {
            return refPoint;
        }
        
        List<CoordinateSystem> stack = getCoordinateStack(refCS);
        CoordinateSystem lastCS = stack.get(stack.size() - 1);
        if (lastCS.getType() != CoordinateSystemTypeEnum.GEOGRAPHIC) {
            return null;
        }

        double[][] trans = getMatrixWithoutGeotransform(refCS, lastCS.getName());
        double[][] coords = new double[1][];
        coords[0] = refPoint.getRefCoord();
        double[][] coordsOut = MatrixUtil.transform(trans, coords, false);
        return new ReferencePoint(lastCS.getName(), coordsOut[0]);
    }
}
